﻿Imports Aricie.DNN.Modules.LuceneSearch.Business
Imports System.Text.RegularExpressions
Imports DotNetNuke.Entities.Portals
Imports DotNetNuke.Common.Utilities
Imports DotNetNuke.Services.FileSystem
Imports Aricie.DNN.Modules.LuceneSearch.Settings
Imports Aricie.DNN.Settings
Imports System.Linq
Imports DotNetNuke.Services.Search
Imports Aricie.Collections

Namespace Aricie.DNN.Modules.LuceneSearch.ModuleProviders

    Public Class KeyToDataHolder(Of K, D As {New})
        Private _KTDBucket As New Dictionary(Of K, D)

        Default Public ReadOnly Property ForKey(k As K) As D
            Get
                If (Not _KTDBucket.ContainsKey(k)) Then
                    _KTDBucket.Add(k, New D())
                End If
                Return _KTDBucket(k)
            End Get
        End Property

        Public ReadOnly Property Keys As IEnumerable(Of K)
            Get
                ' Skip(0) pour avoir quelque chose de réellement non modifiable
                Return _KTDBucket.Keys.Skip(0)
            End Get
        End Property

    End Class


    ''' <summary>
    ''' Ce provider permet de repasser sur l'ensemble des items ramenés par la recherche DotNetNuke et les extensions de LuceneSearch. 
    ''' Il analyse le champ Content et description de chaque LuceneSearchItemInfo pour trouver des liens vers des éléments appartenant au portail courant
    ''' On tente ensuite de retrouver pour chaque lien un identifiant de fichier que l'on traite comme le ferait le StandAloneDocumentProvider et qu'on rajoute à la liste des éléments recherchés
    ''' </summary>
    ''' <remarks></remarks>
    Public Class ReflexiveLinkedDocumentProvider
        Implements ILuceneReflexiveProvider

        Public Function EnrichSearchItems(searchItems As DotNetNuke.Services.Search.SearchItemInfoCollection) As System.Collections.Generic.List(Of LuceneSearchItemInfo) Implements ILuceneReflexiveProvider.EnrichSearchItems
            Dim PortalToFileBucket As KeyToDataHolder(Of Integer, HashSet(Of Integer)) = RetrieveFileIds(searchItems)
            Return ProcessFileIds(PortalToFileBucket)
        End Function

        Function RetrieveFileIds(searchItems As DotNetNuke.Services.Search.SearchItemInfoCollection) As KeyToDataHolder(Of Integer, HashSet(Of Integer))
            Dim PortalIdToLinkClickRegex As New Dictionary(Of Integer, Regex)
            Dim PortalIdToDirectFileRegex As New Dictionary(Of Integer, Regex)
            Dim PortalToFileBucket As New KeyToDataHolder(Of Integer, HashSet(Of Integer))

            Dim FC As New FileController()

            For Each searchItem As DotNetNuke.Services.Search.SearchItemInfo In searchItems
                Dim lsii As LuceneSearchItemInfo = DirectCast(searchItem, LuceneSearchItemInfo)
                Dim settings As LucenePortalSettings = SettingsController.GetModuleSettings(Of LucenePortalSettings)(SettingsScope.PortalSettings, lsii.PortalId)
                For Each s As String In New String() {lsii.Content, lsii.Description} ' les deux champs qui nous intéressent, on pourra étendre plus tard mais pour l'instant ce n'est pas nécessaire

                    If (Not settings.EnableReflexiveDocumentIndexation OrElse String.IsNullOrEmpty(s) OrElse lsii.IndexingStatus = IndexingItemStatus.Skipped) Then
                        Continue For
                    End If

                    For Each m As Match In GetRegexFromPortal(lsii.PortalId, PortalIdToLinkClickRegex, AddressOf GetLinkClickRegex).Matches(s)
                        Try
                            Dim FileId As Integer = UrlUtils.DecryptParameter(m.Groups("Ticket").Value)
                            PortalToFileBucket(lsii.PortalId).Add(FileId)
                        Catch ex As Exception
                            ' que faire que faire...?
                        End Try
                    Next

                    For Each m As Match In GetRegexFromPortal(lsii.PortalId, PortalIdToDirectFileRegex, AddressOf GetDirectFileRegex).Matches(s)
                        Try
                            Dim FileId As Integer = FC.ConvertFilePathToFileId(m.Groups("DirectPath").Value, lsii.PortalId)
                            PortalToFileBucket(lsii.PortalId).Add(FileId)
                        Catch ex As Exception
                            ' que faire que faire...?
                        End Try
                    Next
                Next
            Next

            Return PortalToFileBucket
        End Function


        Function ProcessFileIds(PortalToFileBucket As KeyToDataHolder(Of Integer, HashSet(Of Integer))) As System.Collections.Generic.List(Of LuceneSearchItemInfo)
            ' on se retrouve avec un bucket d'identifiant en fonction du portail
            ' on va gérer tous les éléments mais ne pas récupérer le contenu de ceux qui arriveront après un certain temps configuré
            Dim results As New List(Of LuceneSearchItemInfo)
            Dim sw As Stopwatch = Nothing ' l'élément qui va mesurer l'exécution du temps pour chaque portail

            For Each portalId As Integer In PortalToFileBucket.Keys
                Dim settings As LucenePortalSettings = SettingsController.GetModuleSettings(Of LucenePortalSettings)(SettingsScope.PortalSettings, portalId)
                Dim TimeWindowEndInMilliseconds As Long = settings.ReflexiveDocumentsIndexingTimeWindow * 1000

                Dim luceneDataStore As LuceneSearchDataStore = DirectCast(DotNetNuke.Services.Search.SearchDataStoreProvider.Instance, LuceneSearchDataStore)
                If luceneDataStore IsNot Nothing Then

                    For Each FileId As Integer In PortalToFileBucket(portalId)
                        Try
                            Dim FI As FileInfo = New FileController().GetFileById(FileId, portalId)

                            If (Not settings.ReflexiveDocumentsIndexedExtensionsList.Contains(FI.Extension.ToLowerInvariant)) Then
                                Continue For
                            End If

                            Dim LSII As LuceneSearchItemInfo = StandAloneDocumentProvider.GetSearchItemFromFileId(FI)
                            results.Add(LSII)
                        Catch ex As Exception
                            ' que faire que faire...? Pour l'instant rien
                        End Try
                    Next

                    ' let's retrieve the merged index
                    Dim CurrentlyExistingIndexStructure As Components.Lucene.ExistingIndex.ExistingIndexStructure(Of Components.Lucene.ExistingIndex.ExistingIndexDocumentSmartOrdering) = Nothing
                    Using existingIndex As LuceneQueryResults = luceneDataStore.GetCurrentIndex(portalId)
                        CurrentlyExistingIndexStructure = New Components.Lucene.ExistingIndex.ExistingIndexStructure(Of Components.Lucene.ExistingIndex.ExistingIndexDocumentSmartOrdering)(existingIndex)
                    End Using

                    ' we sort according to the existing index
                    Dim SortedReflexiveItems() As LuceneSearchItemInfo = results.ToArray()
                    Array.Sort(Of SearchItemInfo)(SortedReflexiveItems, New Components.Lucene.IndexMerger.IndexMergeSorter(CurrentlyExistingIndexStructure) With {.OldDocumentsTurnOver = settings.OldDocIndexingRatio})

                    sw = Stopwatch.StartNew()
                    Dim Counter As Integer = 0
                    For Counter = 0 To SortedReflexiveItems.Count - 1
                        Try
                            Dim LSII As LuceneSearchItemInfo = SortedReflexiveItems(Counter)
                            Dim FI As FileInfo = New FileController().GetFileById(LSII.ItemId, portalId)
                            LSII.AdditionalFields = DocumentController.GetDocumentFromFile(FI.PhysicalPath, True)
                            LSII.IndexingStatus = IndexingItemStatus.Ok
                        Catch ex As Exception
                            '?
                        End Try

                        If (sw.ElapsedMilliseconds > TimeWindowEndInMilliseconds) AndAlso (Counter < SortedReflexiveItems.Count - 1) Then
                            For Each LSII As LuceneSearchItemInfo In SortedReflexiveItems.Skip(Counter + 1)
                                LSII.IndexingStatus = IndexingItemStatus.Skipped
                            Next
                            Exit For
                        End If
                    Next
                End If
            Next

            Return results
        End Function

        ''' <summary>
        ''' Permet de récupérer la regex correcte en fonction du portail et la fonction de création passée en paramètre.
        ''' Par ailleurs on conserve cette regex dans un dictionnaire au cas ou le même portail revenait par la suite.
        ''' </summary>
        ''' <param name="portalId"></param>
        ''' <param name="d"></param>
        ''' <param name="create"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Function GetRegexFromPortal(portalId As Integer, d As Dictionary(Of Integer, Regex), create As Func(Of Integer, Regex))
            If Not (d.ContainsKey(portalId)) Then
                d.Add(portalId, create(portalId))
            End If
            Return d(portalId)
        End Function

        Function GetLinkClickRegex(portalId As Integer) As Regex
            Dim regex As String = GetRegex(portalId, "/LinkClick.aspx\?fileticket=(?<Ticket>[^&""']*)")
            Dim options As System.Text.RegularExpressions.RegexOptions = ((System.Text.RegularExpressions.RegexOptions.IgnorePatternWhitespace Or System.Text.RegularExpressions.RegexOptions.Multiline) _
                        Or System.Text.RegularExpressions.RegexOptions.IgnoreCase Or RegexOptions.Compiled)
            Return New System.Text.RegularExpressions.Regex(regex, options)
        End Function

        Function GetDirectFileRegex(portalId As Integer) As Regex
            Dim regex As String = GetRegex(portalId, String.Format("/Portals/{0}/(?<DirectPath>[^""']*)", portalId))
            Dim options As System.Text.RegularExpressions.RegexOptions = ((System.Text.RegularExpressions.RegexOptions.IgnorePatternWhitespace Or System.Text.RegularExpressions.RegexOptions.Multiline) _
                        Or System.Text.RegularExpressions.RegexOptions.IgnoreCase Or RegexOptions.Compiled)
            Return New System.Text.RegularExpressions.Regex(regex, options)
        End Function

        Function GetPortalAliasList(portalId As Integer) As List(Of String)
            Dim ListPortalAlias As New List(Of String)
            For Each PAI As PortalAliasInfo In New PortalAliasController().GetPortalAliasArrayByPortalID(portalId)
                ListPortalAlias.Add(PAI.HTTPAlias)
            Next
            Return ListPortalAlias
        End Function

        Function GetRegex(portalId As String, RegexFilter As String) As String

            ' on veut que le champ attrapé soit dans un tag lien, dans un href.
            Dim regexStart As String = "<a [^>]*href=\s*(""|')\s*{0}"

            Dim VirtualDirectory As String = String.Empty
            If System.Web.HttpContext.Current IsNot Nothing Then ' on gère le fait que lorsque c'est lancé avec une tâche schédulée on ait pas de contexte
                VirtualDirectory = System.Web.HttpContext.Current.Request.ApplicationPath
                If VirtualDirectory = "/" Then
                    VirtualDirectory = String.Empty
                Else
                    VirtualDirectory = String.Format("|{0}", VirtualDirectory)
                End If
            End If

            ' par ailleurs, on veut que l'url commence soit avec un de nos alias, ou notre répertoire partagé
            Dim HttpWithAliasAndVD As String = String.Format("(https?://({0}){1})?", String.Join("|", GetPortalAliasList(portalId).ToArray), VirtualDirectory)

            ' pour finir on rajoute le filtre désiré par l'appellant
            Dim FullDeckedRegex As String = String.Format("{0}{1}", HttpWithAliasAndVD, RegexFilter)

            Return String.Format(regexStart, FullDeckedRegex)
        End Function


    End Class
End Namespace